package com.yunkeji.common.filter;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;

import com.yunkeji.common.constant.AppHttpStatus;
import com.yunkeji.common.enums.app.AppBranchEnum;
import com.yunkeji.common.exception.ServiceException;
import com.yunkeji.common.filter.wrapper.DecryptRequestBodyWrapper;
import com.yunkeji.common.filter.wrapper.EncryptResponseBodyWrapper;
import com.yunkeji.common.utils.EncryptUtils;
import com.yunkeji.common.utils.JsonUtils;
import com.yunkeji.common.utils.ServletUtils;
import com.yunkeji.common.utils.redis.CacheUtils;
import com.yunkeji.common.utils.spring.SpringUtils;

import lombok.Data;
import lombok.SneakyThrows;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

import org.springframework.http.HttpMethod;
import org.springframework.web.servlet.HandlerExceptionResolver;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Crypto 过滤器
 *
 * @author 老张
 */
@Slf4j
public class CryptoFilter implements Filter {

    private List<String> excludedUrls;

    private final CryptoConfig cryptoConfig;

    public CryptoFilter(CryptoConfig cryptoConfig) {
        this.cryptoConfig = cryptoConfig;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化时设置需要排除的URL列表
        excludedUrls =
                Arrays.asList(
                        "api-docs",
                        "swagger",
                        "favicon",
                        "/test/test",
                        // 数据上报回调接口
                        "/system/dataUpload",
                        // 微信支付
                        "/v1/callback/wx/notify_url",
                        // 阿里支付
                        "/v1/callback/ali/notify_url",
                        // 腾讯语音-房间
                        "/tencentrtc/call/roomCallback",
                        // 腾讯语音-录音
                        "/tencentrtc/call/recordCallback",
                        // 腾讯语音-录音实时监测
                        "/tencentrtc/call/rtcContentCheck",
                        // 开始录音回调
                        "/tencentrtc/call/startRecord",
                        // 阿里文件
                        "/v1/file/aliCallback/",
                        // 获取下载链接
                        "/v1/invited/queryDownLoad",
                        // 小猪回调-电签
                        "/v1/piggy/piggyCallBack/piggySignCallBack",
                        // 小猪回调-付款
                        "/v1/piggy/piggyCallBack/piggyPayCallBack");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest servletRequest = (HttpServletRequest) request;
        HttpServletResponse servletResponse = (HttpServletResponse) response;

        String requestURI = ((HttpServletRequest) request).getRequestURI();
        ServletRequest requestWrapper = null;
        ServletResponse responseWrapper = null;
        EncryptResponseBodyWrapper responseBodyWrapper = null;
        // 验证请求头参数是否 完整
        //        branch	客户端渠道号	安卓：1xx，iOS：2xx
        //        serial	自增版本号	例如：23
        String nonce = ((HttpServletRequest) request).getHeader("nonce");
        String branch = ((HttpServletRequest) request).getHeader("branch");
        String serial = ((HttpServletRequest) request).getHeader("serial");
        String model = ((HttpServletRequest) request).getHeader("model");
        String deviceToken = ((HttpServletRequest) request).getHeader("deviceToken");
        String os = ((HttpServletRequest) request).getHeader("os");

        // 以下链接不过滤
        if (CollectionUtil.isNotEmpty(excludedUrls)
                && excludedUrls.stream().anyMatch(item -> requestURI.contains(item))) {
            chain.doFilter(
                    ObjectUtil.defaultIfNull(requestWrapper, request),
                    ObjectUtil.defaultIfNull(responseWrapper, response));
            return;
        }
        byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
        String requestBody = new String(readBytes, StandardCharsets.UTF_8);
        log.info(
                "请求头参数=>{},{},{},{},{},{},请求体参数,{}",
                nonce,
                branch,
                serial,
                model,
                deviceToken,
                os,
                requestBody);
        // 响应默认需要加密
        boolean responseFlag = cryptoConfig.getResenabled();
        // 过滤IP和设备
        if (!ipAndDeviceFilter(servletRequest, servletResponse, responseFlag)) {
            return;
        }
        if (StrUtil.isEmpty(nonce)
                || StrUtil.isEmpty(branch)
                || StrUtil.isEmpty(serial)
                || StrUtil.isEmpty(model)
                || StrUtil.isEmpty(deviceToken)
                || StrUtil.isEmpty(os)) {
            throwException(
                    servletRequest,
                    servletResponse,
                    "some params are empty",
                    AppHttpStatus.参数错误,
                    responseFlag);
            return;
        }

        if (!NumberUtil.isNumber(nonce) || !NumberUtil.isNumber(branch)) {
            throwException(
                    servletRequest,
                    servletResponse,
                    " params must be number ",
                    AppHttpStatus.参数错误,
                    responseFlag);
            return;
        }

        try {
            AppBranchEnum.getByCode(Integer.parseInt(branch));
        } catch (ServiceException e) {
            throwException(
                    servletRequest,
                    servletResponse,
                    "branch is error ",
                    AppHttpStatus.参数错误,
                    responseFlag);
            return;
        } catch (Exception e) {
            throwException(
                    servletRequest,
                    servletResponse,
                    "branch not expect ",
                    AppHttpStatus.参数错误,
                    responseFlag);
            return;
        }

        if (JSONUtil.isTypeJSON(requestBody)) {
            throwException(
                    servletRequest,
                    servletResponse,
                    "param is error",
                    AppHttpStatus.参数错误,
                    responseFlag);
            return;
        }

        if (DateUtil.currentSeconds() - Long.valueOf(nonce) >= cryptoConfig.getExpireApi() * 1000) {
            throwException(
                    servletRequest,
                    servletResponse,
                    "request is expire",
                    AppHttpStatus.重新请求,
                    responseFlag);
            return;
        }

        // 是否为 put 或者 post 请求
        if (HttpMethod.PUT.matches(servletRequest.getMethod())
                || HttpMethod.POST.matches(servletRequest.getMethod())) {
            // 是否存在加密标头
            try {
                // 请求解密
                requestWrapper =
                        new DecryptRequestBodyWrapper(
                                servletRequest, cryptoConfig, Long.valueOf(nonce));
                log.info("请求体参数：{}", JsonUtils.toJsonString(ServletUtils.getParamMap(requestWrapper)));
            } catch (Exception e) {
                log.error(e.getMessage(), ExceptionUtil.getRootCause(e));
                throwException(
                        servletRequest,
                        servletResponse,
                        "decrypt is error",
                        AppHttpStatus.参数错误,
                        responseFlag);
                return;
            }
        }

        // 判断是否响应加密
        if (responseFlag) {
            responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
            responseWrapper = responseBodyWrapper;
        }

        chain.doFilter(
                ObjectUtil.defaultIfNull(requestWrapper, request),
                ObjectUtil.defaultIfNull(responseWrapper, response));

        if (responseFlag) {
            servletResponse.reset();
            // 对原始内容加密PlusWebInvokeTimeInterceptor
            String encryptContent =
                    responseBodyWrapper.getEncryptContent(
                            servletResponse, cryptoConfig.getPublicKey());
            //  对加密后的内容写出
            servletResponse.getWriter().write(encryptContent);
        }
    }

    /**
     * 过滤封禁的IP和设备
     *
     * @param req
     * @param resp
     * @param responseFlag
     */
    private boolean ipAndDeviceFilter(
            HttpServletRequest req, HttpServletResponse resp, boolean responseFlag) {
        String device = req.getHeader("deviceToken");
        if (null == device || device.isEmpty()) {
            return true;
        }
        String ipAddr = ServletUtils.getClientIP(req);
        if (null == ipAddr || ipAddr.isEmpty()) {
            return true;
        }
        String target = CacheUtils.get("banned", device);
        if (null == target || target.isEmpty()) {
            target = CacheUtils.get("banned", ipAddr);
            if (null == target || target.isEmpty()) {
                return true;
            }
        }
        throwException(
                req, resp, "ip or device is banned", AppHttpStatus.当前IP或设备已被封禁, responseFlag);
        return false;
    }

    @SneakyThrows
    private void throwException(
            HttpServletRequest servletRequest,
            HttpServletResponse servletResponse,
            String msg,
            Integer code,
            boolean responseFlag) {

        if (responseFlag) {
            JSONObject errorCode = JSONUtil.createObj();
            //  对加密后的内容写出
            errorCode.set("code", code);
            errorCode.set("msg", msg);
            log.error("客户端异常信息:{}", errorCode.toString());
            String s = EncryptUtils.encryptByRsa(errorCode.toString(), cryptoConfig.getPublicKey());
            servletResponse.getWriter().write(s);
            return;
        }
        // header 需要 AES 加密头标识
        HandlerExceptionResolver exceptionResolver =
                SpringUtils.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
        exceptionResolver.resolveException(
                servletRequest, servletResponse, null, new ServiceException(msg, code));
    }

    @Override
    public void destroy() {}

    @Data
    @ToString
    /** 配置 */
    public class CryptoConfig {
        /** 全局密开关 */
        private Boolean enabled;

        /** 响应加密开关 */
        private Boolean resenabled;

        /** 头部标识 */
        private String headerFlag;

        /** 响应加密公钥 */
        private String publicKey;

        /** 请求解密私钥 */
        private String privateKey;

        /** Api过期时间 */
        private Long expireApi;
    }
}
